package uk.co.hs.service.admin;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.annotation.Resource;

import org.apache.commons.lang3.RandomStringUtils;
import org.hibernate.Hibernate;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import uk.co.hs.constants.SecurityConstants;
import uk.co.hs.dao.web.admin.WebForwardingDao;
import uk.co.hs.dao.web.admin.WebRoleDao;
import uk.co.hs.dao.web.admin.WebRoleUrlDao;
import uk.co.hs.dao.web.admin.WebUrlDao;
import uk.co.hs.dao.web.admin.WebUserDao;
import uk.co.hs.domain.web.WebForwarding;
import uk.co.hs.domain.web.WebPasswordHistory;
import uk.co.hs.domain.web.WebRole;
import uk.co.hs.domain.web.WebRoleUrl;
import uk.co.hs.domain.web.WebUrl;
import uk.co.hs.domain.web.WebUser;
import uk.co.hs.exceptions.DataAccessExceptions;
import uk.co.hs.service.admin.interfaces.WebUserServiceInterface;
import uk.co.hs.web.util.AdminListDTO;

/**
 * WebUserService.
 */
@Service("hs_webUserService")
@Transactional(value="hs_transactionManager")
public class WebUserService implements WebUserServiceInterface
{
  @Resource
  private WebUserDao mWebUserDao;

  @Resource
  private WebRoleDao mWebRoleDao;

  @Resource
  private WebUrlDao mWebUrlDao;

  @Resource
  private WebRoleUrlDao mWebRoleUrlDao;

  @Resource
  private WebForwardingDao mWebForwardingDao;

  @Value("${password.policy.max.failed.logins}")
  private int mMaxNumberOfFailedLogins;

  @Value("${password.policy.must.contain.lowercase}")
  private boolean mMustContainLowerCase;

  @Value("${password.policy.must.contain.uppercase}")
  private boolean mMustContainUpperCase;

  @Value("${password.policy.must.contain.digit}")
  private boolean mMustContainDigit;

  @Value("${password.policy.days.valid}")
  private int mValidPasswordPeriodDays;

  @Value("${password.policy.minimum.length}")
  private int mMinimumLength;

  private Md5PasswordEncoder mMd5PasswordEncoder = new Md5PasswordEncoder();

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getAll()
   * {@inheritDoc}
   */
  public List<WebUser> getAll()
  {
    return mWebUserDao.getAll();
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getWebUser(java.lang.String)
   * {@inheritDoc}
   */
  public WebUser getWebUser(String aUsername)
  {
    return mWebUserDao.getWebUser(aUsername);
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getWebUser(java.lang.Long)
   * {@inheritDoc}
   */
  public WebUser getWebUser(Long aId)
  {
    WebUser webUser = mWebUserDao.getById(aId);
    Hibernate.initialize(webUser.getPasswordHistory());
    return webUser;
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getAllUsersAllowedUrls()
   * {@inheritDoc}
   */
  public ConcurrentMap<String, ConcurrentMap<String, Boolean>> getAllUsersAllowedUrls()
  {
    ConcurrentMap<String, ConcurrentMap<String, Boolean>> map = new ConcurrentHashMap<String, ConcurrentMap<String, Boolean>>();
    List<WebUser> allUsers = mWebUserDao.getAll();
    ConcurrentMap<String, Boolean> currentUrlMap;
    List<WebRoleUrl> currentRoleUrls;
    String currentUrlString;
    for (WebUser user : allUsers)
    {
      if (user.getEndDate() == null && !user.getLocked() && user.getActive())
      {
        currentUrlMap = new ConcurrentHashMap<String, Boolean>();

        // RoleUrls
        currentRoleUrls = mWebRoleUrlDao.getWebRoleUrls(user.getRole());
        for (WebRoleUrl roleUrl : currentRoleUrls)
        {
          currentUrlString = roleUrl.getUrl().getUrl();
          if (currentUrlMap.containsKey(currentUrlString))
          {
            // Always have non-editable as precedent - minimum level
            if (currentUrlMap.get(currentUrlString) && !roleUrl.getEditable())
            {
              currentUrlMap.put(currentUrlString, roleUrl.getEditable());
            }
          }
          else
          {
            currentUrlMap.put(currentUrlString, roleUrl.getEditable());
          }
        }
        map.put(user.getUsername(), currentUrlMap);
      }
    }
    return map;
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getAllUserRoles()
   */
  public ConcurrentMap<String, String> getAllUserRoles()
  {
    ConcurrentMap<String, String> allUserRoles = new ConcurrentHashMap<String, String>();
    List<WebUser> allWebUsers = mWebUserDao.getAll();
    for (WebUser webUser: allWebUsers)
    {
      if (webUser.getEndDate() == null && !webUser.getLocked() && webUser.getActive())
      {
        allUserRoles.put(webUser.getUsername(), webUser.getRole().getName());
      }
    }
    return allUserRoles;
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getAllowedUrls(java.lang.String)
   * {@inheritDoc}
   */
  public Map<String, Boolean> getAllowedUrls(String aUsername)
  {
    WebUser user = mWebUserDao.getWebUser(aUsername);
    if (user == null)
    {
      return new HashMap<String, Boolean>();
    }

    Map<String, Boolean> map = new HashMap<String, Boolean>();
    String currentUrlString;

    // RoleUrls
    List<WebRoleUrl> roleUrls = mWebRoleUrlDao.getWebRoleUrls(user.getRole());
    for (WebRoleUrl roleUrl : roleUrls)
    {
      currentUrlString = roleUrl.getUrl().getUrl();
      if (map.containsKey(currentUrlString))
      {
        // Always have non-editable as precedent - minimum level
        if (map.get(currentUrlString) && !roleUrl.getEditable())
        {
          map.put(currentUrlString, roleUrl.getEditable());
        }
      }
      else
      {
        map.put(currentUrlString, roleUrl.getEditable());
      }
    }
    return map;
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getRole(java.lang.Long)
   * {@inheritDoc}
   */
  public WebRole getRole(Long aPk)
  {
    return mWebRoleDao.getById(aPk);
  }

  public WebUrl getUrl(Long aPk)
  {
    return mWebUrlDao.getById(aPk);
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getWebUserIncludingPasswordHistory(java.lang.String)
   * {@inheritDoc}
   */
  public WebUser getWebUserIncludingPasswordHistory(String aUserName)
  {
    return mWebUserDao.getWebUserIncludingPasswordHistory(aUserName);
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getAllSortedRecords()
   * {@inheritDoc}
   */
  public List<AdminListDTO> getAllSortedRecords()
  {
    List<AdminListDTO> listItems = new ArrayList<AdminListDTO>();
    List<Object> currentObjectList;
    WebUser currentUser;
    List<WebUser> users = mWebUserDao.getAllWebUsers();

    List<Object> currentList;
    int userCount = users.size();
    for (int index = 0; index < userCount; index++)
    {
      currentUser = users.get(index);
      currentObjectList = new ArrayList<Object>();
      currentObjectList.add(currentUser.getUsername());
      if (currentUser.getRole() != null)
      {
        currentObjectList.add(currentUser.getRole().getName());
      }
      else
      {
        currentObjectList.add("");
      }
      currentObjectList.add(currentUser.getActive());
      currentObjectList.add(!currentUser.getLocked());

      currentList = new ArrayList<Object>();
      currentList.add(currentUser.getPk());

      listItems.add(new AdminListDTO(currentList, currentObjectList));
    }
    return listItems;
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #delete(uk.co.hs.domain.web.WebUser)
   * {@inheritDoc}
   */
  @Transactional(rollbackFor=Exception.class)
  public void delete(WebUser aUser) throws DataIntegrityViolationException, HibernateOptimisticLockingFailureException, DataAccessException, Exception
  {
    aUser.setEndDate(new DateTime());
    aUser.setLastUpdated(new DateTime());
    mWebUserDao.makePersistent(aUser);
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #save(uk.co.hs.domain.web.WebUser)
   * {@inheritDoc}
   */
  @Transactional(rollbackFor=Exception.class)
  public void save(WebUser aUser) throws DataIntegrityViolationException, HibernateOptimisticLockingFailureException, DataAccessException, Exception
  {
    aUser.setLastUpdated(new DateTime());
    mWebUserDao.makePersistent(aUser);
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #saveUserAndLinkRoleAndLinkFowarding(uk.co.hs.domain.web.WebUser, java.lang.Long, java.lang.Long, boolean)
   * {@inheritDoc}
   */
  @Transactional(rollbackFor=Exception.class)
  public String saveUserAndLinkRoleAndLinkFowarding(WebUser aUser,
                                                    Long aRolePk,
                                                    Long aForwardingUrlPk,
                                                    boolean aIsNew) throws DataIntegrityViolationException, HibernateOptimisticLockingFailureException, DataAccessException, Exception
  {
    // Role
    WebRole roleFound = getRole(aRolePk);
    if (aRolePk != null && roleFound == null)
    {
      return DataAccessExceptions.USER_SAVE_ROLE_DOESNT_EXIST;
    }

    // Url
    WebUrl urlFound = getUrl(aForwardingUrlPk);
    if (aForwardingUrlPk != null && urlFound == null)
    {
      return DataAccessExceptions.USER_SAVE_FORWARDING_DOESNT_EXIST;
    }

    WebUser userToSave = null;
    aUser.setRole(roleFound);

    int consecutiveFailedLogins = 0;
    if (aIsNew)
    {
      aUser.setPassword(encodePassword(aUser.getPassword()));
      userToSave = new WebUser();
      userToSave.setConsecutiveFailedLogins(0);
    }
    else
    {
      userToSave = getWebUser(aUser.getPk());
      if (userToSave.getLocked() && !aUser.getLocked())
      {
        consecutiveFailedLogins = 0;
      }
      else
      {
        consecutiveFailedLogins = aUser.getConsecutiveFailedLogins();
      }
    }

    userToSave.setActive(aUser.getActive());
    userToSave.setConsecutiveFailedLogins(consecutiveFailedLogins);
    userToSave.setEndDate(aUser.getEndDate());
    userToSave.setLastPasswordChanged(aUser.getLastPasswordChanged());
    userToSave.setLocked(aUser.getLocked());
    userToSave.setNewPasswordRequired(aUser.getNewPasswordRequired());
    userToSave.setPassword(aUser.getPassword());
    userToSave.setRole(aUser.getRole());
    userToSave.setUsername(aUser.getUsername());

    mWebUserDao.makePersistent(userToSave);

    // Save forwarding
    if (aIsNew)
    {
      WebForwarding newWebForwarding = new WebForwarding();
      newWebForwarding.setWebUser(userToSave);
      newWebForwarding.setWebUrl(urlFound);
      mWebForwardingDao.makePersistent(newWebForwarding);
    }
    else
    {
      WebForwarding currentWebForwarding = mWebForwardingDao.getWebForwarding(aUser.getUsername());
      if (currentWebForwarding != null)
      {
        if (!currentWebForwarding.getWebUrl().getPk().equals(urlFound.getPk()))
        {
          currentWebForwarding.setWebUrl(urlFound);
          mWebForwardingDao.makePersistent(currentWebForwarding);
        }
      }
      else
      {
        WebForwarding newWebForwarding = new WebForwarding();
        newWebForwarding.setWebUser(userToSave);
        newWebForwarding.setWebUrl(urlFound);
        mWebForwardingDao.makePersistent(newWebForwarding);
      }
    }

    return null;
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #changePassword(java.lang.String, java.lang.String)
   * {@inheritDoc}
   */
  @Transactional(rollbackFor=Exception.class)
  public void changePassword(String aUserName,
                             String aNewUnencodedPassword) throws DataIntegrityViolationException, HibernateOptimisticLockingFailureException, DataAccessException, Exception
  {
    WebUser userFromDb = getWebUserIncludingPasswordHistory(aUserName);
    if (userFromDb != null)
    {
      // Store history
      userFromDb.getPasswordHistory().add(new WebPasswordHistory(userFromDb.getPassword()));

      // Encode password
      String encodedPassword = encodePassword(aNewUnencodedPassword);
      userFromDb.setPassword(encodedPassword);

      // Set last saved password date to current date midnight
      userFromDb.setLastPasswordChanged(new DateTime().toDateMidnight().toDateTime());
      userFromDb.setNewPasswordRequired(false);

      // Reset consecutive logins
      userFromDb.setConsecutiveFailedLogins(0);

      save(userFromDb);
    }
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #resetPassword(java.lang.String)
   * {@inheritDoc}
   */
  @Transactional(rollbackFor=Exception.class)
  public String[] resetPassword(String aUserName) throws DataIntegrityViolationException, HibernateOptimisticLockingFailureException, DataAccessException, Exception
  {
    String[] returnStrings = new String[2];

    WebUser userToReset = getWebUser(aUserName);
    if (userToReset == null)
    {
      returnStrings[0] = DataAccessExceptions.ALREADY_DELETED;
      returnStrings[1] = null;
      return returnStrings;
    }

    userToReset.setConsecutiveFailedLogins(0);
    userToReset.setNewPasswordRequired(Boolean.TRUE);

    String newUnEncodedPassword = generateRandomPasswordString();
    String newEncodedPassword = encodePassword(newUnEncodedPassword);
    userToReset.setPassword(newEncodedPassword);

    save(userToReset);

    returnStrings[0] = null;
    returnStrings[1] = newUnEncodedPassword;
    return returnStrings;
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #passwordsMatch(java.lang.String, java.lang.String)
   * {@inheritDoc}
   */
  public boolean passwordsMatch(String aUserName,
                                String aPasswordEntered)
  {
    WebUser userFromDb = getWebUser(aUserName);
    if (userFromDb != null)
    {
      String encodedPassword = encodePassword(aPasswordEntered);
      if (encodedPassword.equals(userFromDb.getPassword()))
      {
        return true;
      }
    }
    return false;
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #incrementFailedLoginAttempts(java.lang.String)
   * {@inheritDoc}
   */
  @Transactional(rollbackFor=Exception.class)
  public String incrementFailedLoginAttempts(String aUsername)
  {
    WebUser userFromDb = getWebUser(aUsername);
    if (userFromDb != null)
    {
      if (userFromDb.getConsecutiveFailedLogins() == mMaxNumberOfFailedLogins || userFromDb.getLocked())
      {
        return SecurityConstants.LOCKED;
      }
      else if (!userFromDb.getActive())
      {
        return SecurityConstants.ACTIVE;
      }
      else if (!aUsername.equals(WebUser.UTILISOFT_USER) && getCredentialsExpired(userFromDb.getNewPasswordRequired(), userFromDb.getLastPasswordChanged()))
      {
        return SecurityConstants.EXPIRED;
      }
      mWebUserDao.incrementFailedLoginAttempts(userFromDb);
      return SecurityConstants.PASSWORD;
    }
    return SecurityConstants.USER;
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #encodePassword(java.lang.String)
   * {@inheritDoc}
   */
  public String encodePassword(String aPassword)
  {
    return mMd5PasswordEncoder.encodePassword(aPassword, null);
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #resetFailedLoginAttempts(java.lang.String)
   * {@inheritDoc}
   */
  @Transactional(rollbackFor=Exception.class)
  public void resetFailedLoginAttempts(String aUsername)
  {
    WebUser userFromDb = getWebUser(aUsername);
    if (userFromDb != null)
    {
      mWebUserDao.resetFailedLoginAttempts(userFromDb);
    }
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #lockUser(java.lang.String)
   * {@inheritDoc}
   */
  @Transactional(rollbackFor=Exception.class)
  public void lockUser(String aUsername)
  {
    WebUser userFromDb = getWebUser(aUsername);
    if (userFromDb != null)
    {
      mWebUserDao.lockUser(userFromDb);
    }
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #generateRandomPasswordString()
   * {@inheritDoc}
   */
  public String generateRandomPasswordString()
  {
    boolean alphaRequired = false;
    if (mMustContainLowerCase || mMustContainUpperCase)
    {
      alphaRequired = true;
    }
    return RandomStringUtils.random(mMinimumLength, alphaRequired, mMustContainDigit);
  }

  /**
   * @see uk.co.hs.service.admin.interfaces.WebUserServiceInterface
   * #getCredentialsExpired(org.joda.time.DateTime)
   * {@inheritDoc}
   */
  public boolean getCredentialsExpired(Boolean aGetNewPasswordExpired, DateTime aLastPasswordChangeDate)
  {
    if (aGetNewPasswordExpired)
    {
      return true;
    }
    if (aLastPasswordChangeDate == null)
    {
      return true;
    }
    DateTime currentDate = new DateTime().toDateMidnight().toDateTime();
    DateTime expiryDate = aLastPasswordChangeDate.plusDays(mValidPasswordPeriodDays);
    if (currentDate.isAfter(expiryDate))
    {
      return true;
    }
    return false;
  }
}
